package tex.parser;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.Vector;
import java.util.regex.Pattern;

import tex.base.Definition;
import tex.base.Document;
import tex.base.Element;
import tex.base.Enviroment;
import tex.base.Preamble;
import tex.base.Tex;
import tex.base.Text;

public class TexParser {
	private BufferedReader reader;
	private Pattern token;
	private Pattern white;
	private Pattern simpleText;

	public TexParser(File file) throws FileNotFoundException {
		reader = new BufferedReader(new FileReader(file));
		token = Pattern.compile("[^\\\\,\\[\\]\\{\\} \n]");
		white = Pattern.compile("[ \t\n]");
		simpleText = Pattern.compile("[^\\\\\\[\\]\\{\\}]");
	}

	public Tex compile() throws IOException, ParseException {
		return TexGenerator();
	}

	private Tex TexGenerator() throws IOException, ParseException {
		Tex tex = new Tex();
		
		System.out.println("Tex");

		tex.setPreamble(PreAmbleGenerator());
		tex.setDocument(DocumentGenerator());

		return tex;
	}

	private Document DocumentGenerator() throws IOException, ParseException {
		System.out.println("Doc");
		if (consumeInput("\\begin")) {
			Document document = new Document();
			Vector<String> opts;
			if ((opts = OptionsGenerator()) != null)
				document.getOptions().addAll(opts);
			if (!(consumeInput("{") && consumeInput("document") && consumeInput("}")))
				throw new ParseException("Error at line: " + reader.readLine() + "\nexpected: '{' %document '}'");

			Element e;

			while ((e = ElementGenerator()) != null) {
				document.getCorpo().add(e);
			}
			
			if (consumeInput("\\end{") && consumeInput("document")
					&& consumeInput("}"))
				return document;
			else
				throw new ParseException("Error at line: " + reader.readLine() + "\nexpected: '\\end{document}'");
		} else {
			return null;
		}
	}

	private Preamble PreAmbleGenerator() throws IOException, ParseException {
		System.out.println("Pre");
		Preamble preamble = new Preamble();
		Definition d;

		while ((d = DefinitionGenerator()) != null) {
			preamble.getDefinitions().add(d);
		}

		return preamble;
	}

	private Definition DefinitionGenerator() throws IOException, ParseException {
		System.out.println("Def");
		Definition definition = new Definition();
		Enviroment e = null;
		Vector<String> opts = null;
		
		while (consumeInput(white) != null || consumeComment());
		
		if (checkInput("\\begin") || checkInput("\\end")) {
			return null;
		} else if (consumeInput("\\")) {
			String name = consumeInput(token);
			if (name != null) {
				definition.setName(name);
			} else
				throw new ParseException("Error at line: " + reader.readLine() + "\nexpected: a name");

			while (((e = EnviromentGenerator()) != null)
					|| ((opts = OptionsGenerator()) != null)) {
				if (e != null)
					definition.getEnviroments().add(e);
				if (opts != null)
					definition.getOptions().addAll(opts);
				e = null;
				opts = null;
			}

			return definition;
		} else {
			return null;
		}
	}

	private Vector<String> OptionsGenerator() throws IOException,
			ParseException {
		System.out.println("Ops");
		if (consumeInput("[")) {
			String op;
			Vector<String> options = new Vector<String>();
			consumeInput(white);
			while ((op = consumeInput(token)) != null) {
				options.add(op);
				consumeInput(white);
				consumeInput(",");
				consumeInput(white);
			}
			if (consumeInput("]"))
				return options;
			else
				throw new ParseException("Error at line: " + reader.readLine() + "\nexpected: ']'");
		} else {
			return null;
		}
	}

	private Enviroment EnviromentGenerator() throws IOException, ParseException {
		System.out.println("Env");
		if (consumeInput("{")) {
			Enviroment enviroment = new Enviroment();
			Element e;
			while ((e = ElementGenerator()) != null) {
				enviroment.getElements().add(e);
			}
			if (consumeInput("}"))
				return enviroment;
			else
				throw new ParseException("Error at line: " + reader.readLine() + "\nexpected: '}'");
		} else if (consumeInput("\\begin")) {
			Enviroment enviroment = new Enviroment();
			Vector<String> opts = OptionsGenerator();
			if (opts != null) enviroment.getOpts().addAll(opts);
			String name;
			if (consumeInput("{") && (name = consumeInput(token)) != null
					&& consumeInput("}")) {
				enviroment.setName(name);
			} else
				throw new ParseException("Error at line: " + reader.readLine() + "\nexpected: a token");
			Element e;

			while ((e = ElementGenerator()) != null) {
				enviroment.getElements().add(e);
			}
			if (consumeInput("\\end{") && consumeInput(enviroment.getName())
					&& consumeInput("}"))
				return enviroment;
			else
				throw new ParseException("Error at line: " + reader.readLine() + "\nexpected: '\\end{" + enviroment.getName() + "}'");
		} else {
			return null;
		}
	}

	private Element ElementGenerator() throws IOException, ParseException {
		System.out.println("Elem");
		Enviroment e;
		Definition d;
		Text t;
		if ((e = EnviromentGenerator()) != null) {
			return e;
		} else if ((d = DefinitionGenerator()) != null) {
			return d;
		} else if ((t = TextGenerator()) != null) {
			return t;
		} else {
			return null;
		}
	}

	private Text TextGenerator() throws IOException {
		System.out.println("Text");
		Text text = new Text();
		String t = consumeInput(simpleText);
		if (t != null) {
			text.getText().add(t);
			return text;
		} else {
			return null;
		}
	}

	private boolean consumeInput(String expected) throws IOException {
		reader.mark(expected.length());
		for (int i = 0; i < expected.length(); i++) {
			if (consumeChar()[0] != expected.charAt(i)) {
				reader.reset();
				return false;
			}
		}
		return true;
	}
	
	private boolean checkInput(String expected) throws IOException {
		reader.mark(expected.length());
		for (int i = 0; i < expected.length(); i++) {
			if (consumeChar()[0] != expected.charAt(i)) {
				reader.reset();
				return false;
			}
		}
		reader.reset();
		return true;
	}

	private String consumeInput(Pattern expected) throws IOException {
		String recovered = "";
		char[] c;

		reader.mark(1);
		while (expected.matcher(new String(c = consumeChar())).find()) {
			recovered += c;
			reader.mark(1);
		}
		reader.reset();
		if (recovered.length() > 0)
			return recovered;
		else
			return null;
	}

	private char[] consumeChar() throws IOException {
		char[] c = new char[1];
		reader.read(c, 0, 1);
		return c;
	}
	
	private boolean consumeComment() throws IOException {
		if (consumeInput("%")) {
			while ((char)reader.read() != '\n');
			return true;
		} else return false;
	}
}
